到目前為止,都還在介紹「既有登入系統」和 Hydra 的功能,接著要來跨出很難的第一步了--整合。
Hydra 的設計把整個登入流程拆分成四個服務:
Hydra 是個具有完整功能的微服務,所以後面不會修改 Hydra 的任何程式碼,但會調整相關的設定。主要是要修改既有系統的程式碼,其中應用程式和 Login Provider 能夠從既有系統中拆出來,而 Consent Provider 則是原本沒有的,因此需要從頭開始寫。因此從這些角度分析,原有系統跟轉換後的系統的樣子應該如下表:
轉換前的行為 | 轉換後的行為 |
---|---|
GET /login 進登入頁 |
GET /login 轉導至 Hydra |
無 | GET /oauth2/login Login Provider,使用原有登入頁 |
POST /login 驗證帳密 |
POST /oauth2/login 改路徑 |
無 | GET /oauth2/consent Consent Provider |
無 | POST /oauth2/consent 執行同意授權 |
無 | GET /callback 接受身分驗證回應的端點 |
主要就是把驗證帳密的部分拆出來,其他與 IdP 相關的功能,如註冊、忘記密碼等,就先不動。
在測試既有系統的登入過程中,觸發登入(和顯示登入頁)的 URL 主要是 /login
,這個連結將會改轉導到 Hydra,然後建立新的路徑來實作 Login Provider 與 Consent Provider,這裡使用 /oauth2/login
與 /oauth2/consent
。
先找到 /login
路由,是在 routes/auth.php
裡面定義,我們先改成一個轉導的寫法,另外同時新增 /callback
路由,是 OAuth 2.0 流程的最後一步會用到的:
// 原本
// Route::get('login', [AuthenticatedSessionController::class, 'create'])
// ->name('login');
// 調整移到最下面
Route::get('login', function() {
return response('先假裝我是轉導');
})->name('login');
Route::get('callback', function() {
dump(request()->all());
return response('拿到身分驗證回應了');
});
再來把它原本對應的 AuthenticatedSessionController::create()
方法,設定給新的 /oauth2/login
路由,可以寫在 routes/auth.php
的最下面,對應的 POST 方法路由,也重新設定路由名稱,並把回應調整過:
Route::get('/oauth2/login', function() {
return view('auth.login');
})->name('oauth2.login');
Route::post('/oauth2/login', function(\App\Http\Requests\Auth\LoginRequest $request) {
$request->authenticate();
$request->session()->regenerate();
return 'OAuth 2.0 身分驗證完成';
});
然後測試看看 http://127.0.0.1:8000/oauth2/login 能不能看到原有的登入頁,可以的話就算成功了。
接著先暫時複製 login 頁面(resources/views/auth/login.blade.php
)來做一個 consent 頁面(resources/views/auth/consent.blade.php
),並設定到 /oauth2/consent
路由,同時也建立一個讓 consent 發送 POST 請求的路由:
Route::get('/oauth2/consent', function() {
return view('auth.consent');
})->name('oauth2.consent');
Route::post('/oauth2/consent', function() {
dump(request()->all());
return 'OAuth 2.0 授權完成';
});
頁面隨便做,能用比較重要:
因為前面的過程有把 DB 重新清理過,這裡再重新註冊一次應用程式。跟之前說明快速開始一樣,但指令和最後的轉導路徑調整一下:
hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
create \
--id my-rp \
--secret my-secret \
--grant-types authorization_code,implicit,client_credentials,refresh_token \
--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
--scope openid \
--token-endpoint-auth-method client_secret_basic \
--callbacks http://127.0.0.1:8000/callback
hydra.yml
的設定需要新增 Login Provider 與 Consent Provider 的位置,記得改完之後要重啟 Hydra。
我們再回頭把 /login
的程式調整一下。還記得快速開始的 Debugger 嗎?依對應必要的欄位,把程式實作出來如下。其中有個欄位是 Authorize URI,這個指的是 Hydra 啟動授權流程的端點:
Route::get('login', function () {
$authorizeUri = 'http://127.0.0.1:4444/oauth2/auth';
$query = \Illuminate\Support\Arr::query([
'client_id' => 'my-rp',
'redirect_uri' => 'http://127.0.0.1:8000/callback',
'scope' => 'openid',
'response_type' => 'code',
'state' => '1a2b3c4d',
]);
return redirect($authorizeUri . '?' . $query);
})->name('login');
接著回首頁再按一次登入,如果前面的步驟有照著做的話,畫面內容會完全一致,但注意看網址,會多一個 login_challenge
的參數,這個是 Hydra 建立並轉導至 Login Provider 的:
看到這個畫面,就先恭喜大家成功完成第一步串接了,執行登入應該就會看到 OAuth 2.0 身分驗證完成
。到這邊為止,我們已經完成了循序圖的第 1 步和第 2 步了,明天來看我們要如何實作第 3 步。
今天的程式碼調整不難,這是因為「既有系統」還算小,所以還很好控制,即便資料庫有共用,但都可以透過 API 包裝來解決。實務上真正會遇到的問題,通常是身分驗證流程與應用程式綁定太多業務邏輯,比方說:有些產品擁有者對於登入定義可能包含了身分驗證與存取控制,而存取控制的邏輯,可能跟產品有直接相關,像需要通過實名認證與管理員授權。因此這些產品擁有者並不認為帳密正確就算登入完成,而是必須包含實名認證與管理員授權,在這個情境下,要把登入獨立拆出來就會非常困難--因為產品跟 Hydra 對於登入這個行為的描述不是共同語言(Ubiquitous Language)。
遇到這個問題,就必須跟產品擁有者溝通關於登入行為的分離。慶幸的是,當有需求做第三方登入驗證的提供者,通常就必須重新定義登入行為,因為原本只有第一方登入的行為,後續要改成可以接受第三方登入,業務邏輯是需要重新思考的。當遇到了,記得好好跟產品擁有者討論即可。
相關的程式碼可以參考 GitHub。